// Copyright (c) 2010 Thomas Stuefe
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

#include <windows.h>
#include <stdio.h>
#include "sqlite3.h"

#include <crtdbg.h>

#include "internals.h"


// define this to trace in UTF16 to a file (default is UTF8)
#define TRACE_UTF16

// just a primitive trace which I use if tracing itself wont work
#define trcSafe(...) { \
	fprintf(stderr, __VA_ARGS__); \
	fputc('\n', stderr); \
	fflush(stderr);  \
}

// helper, return a string describing the sqlite error code
static const wchar_t* describeErrorCode(int errcode) {

#define _X(code)  case code: return L#code;

	switch (errcode) {
		_X(SQLITE_OK)
		_X(SQLITE_ERROR)
		_X(SQLITE_INTERNAL)
		_X(SQLITE_PERM)
		_X(SQLITE_ABORT)
		_X(SQLITE_BUSY)
		_X(SQLITE_LOCKED)
		_X(SQLITE_NOMEM)
		_X(SQLITE_READONLY)
		_X(SQLITE_INTERRUPT)
		_X(SQLITE_IOERR)
		_X(SQLITE_CORRUPT)
		_X(SQLITE_NOTFOUND)
		_X(SQLITE_FULL)
		_X(SQLITE_CANTOPEN)
		_X(SQLITE_PROTOCOL)
		_X(SQLITE_EMPTY)
		_X(SQLITE_SCHEMA)
		_X(SQLITE_TOOBIG)
		_X(SQLITE_CONSTRAINT)
		_X(SQLITE_MISMATCH)
		_X(SQLITE_MISUSE)
		_X(SQLITE_NOLFS)
		_X(SQLITE_AUTH)
		_X(SQLITE_FORMAT)
		_X(SQLITE_RANGE)
		_X(SQLITE_NOTADB)
		_X(SQLITE_ROW)
		_X(SQLITE_DONE)
		_X(SQLITE_IOERR_READ)
		_X(SQLITE_IOERR_SHORT_READ)
		_X(SQLITE_IOERR_WRITE)
		_X(SQLITE_IOERR_FSYNC)
		_X(SQLITE_IOERR_DIR_FSYNC)
		_X(SQLITE_IOERR_TRUNCATE)
		_X(SQLITE_IOERR_FSTAT)
		_X(SQLITE_IOERR_UNLOCK)
		_X(SQLITE_IOERR_RDLOCK)
		_X(SQLITE_IOERR_DELETE)
		_X(SQLITE_IOERR_BLOCKED)
		_X(SQLITE_IOERR_NOMEM)
		_X(SQLITE_IOERR_ACCESS)
		_X(SQLITE_IOERR_CHECKRESERVEDLOCK)
		_X(SQLITE_IOERR_LOCK)
		_X(SQLITE_IOERR_CLOSE)
		_X(SQLITE_IOERR_DIR_CLOSE)
		_X(SQLITE_IOERR_SHMOPEN)
		_X(SQLITE_IOERR_SHMSIZE)
		_X(SQLITE_IOERR_SHMLOCK)
		_X(SQLITE_LOCKED_SHAREDCACHE)
		_X(SQLITE_BUSY_RECOVERY)
		_X(SQLITE_CANTOPEN_NOTEMPDIR)
		default: return L"Unknown Error Code";
	}

#undef _X

} // end: sqliteErrorCodeToString


void TrcBuffer::printf(const wchar_t* format, ...) {
	
	va_list ap;
	va_start(ap, format);
	TrcBuffer::vprintf(format, ap);
	va_end(ap);

}

void TrcBuffer::vprintf(const wchar_t* format, va_list ap) {
	UNMANAGED_ASSERT(_pos >= 0 && _pos < BUFSIZE);
	UNMANAGED_ASSERT(_buf[_pos] == L'\0');
	const int remaining = BUFSIZE - _pos;
	if (remaining > 2) {
		const int written = _vsnwprintf_s(_buf + _pos, remaining, remaining - 1, format, ap);
		UNMANAGED_ASSERT(written >= 0);
		_pos += written;
		UNMANAGED_ASSERT(_pos < BUFSIZE);
		UNMANAGED_ASSERT(_buf[_pos] == L'\0');
	}
}

void TrcBuffer::puts(const wchar_t* s) {
	UNMANAGED_ASSERT(_pos >= 0 && _pos < BUFSIZE);
	UNMANAGED_ASSERT(_buf[_pos] == L'\0');
	UNMANAGED_ASSERT(s);
	int i = 0;
	while (_pos < BUFSIZE - 1 && s[i]) {
		_buf[_pos] = s[i];
		i ++; _pos ++;
	}
	_buf[_pos] = L'\0';
	UNMANAGED_ASSERT(_pos < BUFSIZE);
	UNMANAGED_ASSERT(_buf[_pos] == L'\0');
}

void TrcBuffer::putc(wchar_t c) {
	UNMANAGED_ASSERT(_pos >= 0 && _pos < BUFSIZE);
	UNMANAGED_ASSERT(_buf[_pos] == L'\0');
	UNMANAGED_ASSERT(c != L'\0');
	if (_pos < BUFSIZE - 1) {
		_buf[_pos] = c; 
		_pos ++;
		_buf[_pos] = L'\0';
	}
}

void TrcBuffer::cr() {
	putc(L'\n');
}

void TrcBuffer::tab() {
	putc(L'\t');
}

void TrcBuffer::sanitize() {
	UNMANAGED_ASSERT(_pos >= 0 && _pos < BUFSIZE);
	UNMANAGED_ASSERT(_buf[_pos] == L'\0');
	const wchar_t replacement = L'#';
	for (int i = 0; _buf[i]; i ++) {
		if (_buf[i] < 0x20) {
			if (_buf[i] != L'\r' && _buf[i] != L'\n' && _buf[i] != L'\t') {
				_buf[i] = replacement;
			}
		}
	}
}


int g_traceLevel = 0;
static int g_traceDestination = TRCDST_STDOUT;
static wchar_t* g_traceFileName = NULL;
static HANDLE g_traceFileHandle = INVALID_HANDLE_VALUE;

// close trace file (but preserves the trace file name!). 
// Afterwards, g_tracefileHandle will be NULL.
// The next call to openTraceFile will reopen the trace file.
static void closeTraceFile() {
	
	UNMANAGED_ASSERT(g_traceFileHandle != INVALID_HANDLE_VALUE);
	UNMANAGED_ASSERT(g_traceFileName);

	if (!::FlushFileBuffers(g_traceFileHandle)) {
		trcSafe("FlushFileBuffers failed (%d)", GetLastError());
	}

	if (!::CloseHandle(g_traceFileHandle)) {
		trcSafe("CloseHandle failed (%d)", GetLastError());
	}

	g_traceFileHandle = INVALID_HANDLE_VALUE;

	UNMANAGED_ASSERT(g_traceFileHandle == INVALID_HANDLE_VALUE);
	UNMANAGED_ASSERT(g_traceFileName);

}

// open trace file. g_traceFileName must be set, g_tracefileHandle must be INVALID_HANDLE_VALUE.
static bool openTraceFile() {

	UNMANAGED_ASSERT(g_traceFileHandle == INVALID_HANDLE_VALUE);
	UNMANAGED_ASSERT(g_traceFileName);

	g_traceFileHandle = ::CreateFileW(
		g_traceFileName,
		GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		CREATE_ALWAYS,
		FILE_ATTRIBUTE_NORMAL,
		NULL
		);

	if (!g_traceFileHandle) {
		trcSafe("Could not open trace file, CreateFileW failed (%d)", GetLastError());
		return false;
	}

#ifdef TRACE_UTF16
	// I write all data as UTF16 to avoid conversion. So, I need to place a Byte Order Mark first.
	const wchar_t* const BOM = L"\xFEFF";
	DWORD written = 0;
	::WriteFile(g_traceFileHandle, BOM, sizeof(wchar_t), &written, NULL);
	UNMANAGED_ASSERT(written == sizeof(wchar_t));
#endif

	return true;

}


// this is the function which does the actual tracing. I have problems with using plain stderr, stdout in
// this project - nothing was visible unless "Enable native Debugging" was set in the using (C#) project properties -
// very weird. Just starting the program outside a debugger in cmd.exe also showed nohting.
// Did not find much info on that, apart from that I am not the only one having this problem. No explanation, but
// the recommendation to use OutputDebugString instead...
// So, for now this is what I do, but I feel like I have not really understood the issue here. Is stdout/stderr
// output via C-Runtime not possible at all in a managed context? 
static void __trc0(const wchar_t* text) {

	switch (g_traceDestination) {
	case TRCDST_STDERR:
		fputws(text, stderr); fflush(stderr);
		break;
	case TRCDST_STDOUT:
		fputws(text, stdout); fflush(stdout);
		break;
	case TRCDST_FILE:
		{
			if (g_traceFileHandle == INVALID_HANDLE_VALUE) {
				openTraceFile();
				if (g_traceFileHandle == INVALID_HANDLE_VALUE) {
					trcSafe("Cannot open trace file, reverting to stdout."); 
					g_traceDestination = TRCDST_STDOUT;
					__trc0(text);
					return;
				}
			}
		
			UNMANAGED_ASSERT( g_traceFileHandle != INVALID_HANDLE_VALUE && g_traceFileName );

			DWORD written = 0;
			const size_t len = wcslen(text);
			
#ifdef TRACE_UTF16
			if (!::WriteFile(g_traceFileHandle, text, len * sizeof(wchar_t), &written, NULL)) {
				trcSafe("WriteFile failed (%d)", GetLastError()); 
			}
#else
			WideCharToMultiByte
#endif
		}
		break;
	case TRCDST_DBGVIEW:
		::OutputDebugStringW(text);
		break;
	}

}

// trace some information when tracing starts (time, date, sqlite version etc)
static void traceHeader() {
	
	if (g_traceLevel > 0) {
		TrcBuffer buf;
	
		buf.puts(L"zencat sqlite trace.");
		buf.cr();
	
		int v = ::sqlite3_libversion_number();
		const int v1 = v / 1000000;
		v -= v1 * 1000000;
		const int v2 = v / 1000;
		v -= v2 * 1000;
		const int v3 = v;
		buf.printf(L"sqlite version: %d.%d.%d", v1, v2, v3);
		buf.cr();

		SYSTEMTIME time;
		GetSystemTime(&time);
		buf.printf(L"%d-%d-%d %d:%d:%d ",
			time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond);
		buf.cr();

		buf.sanitize();
		__trc0(buf.ptr());
	}
}


// Each traced line starts with a header containing date, time, thread id, message severity
static void traceLineStart(TrcContext* p_context) {

	if (p_context->buffer.full()) {
		return;
	}

	UNMANAGED_ASSERT(p_context);

	const DWORD threadid = GetCurrentThreadId();
	const char* const severityStrings [] = { "", "- *** Error", "", "", "" };
		
	SYSTEMTIME time;
	GetSystemTime(&time);

	p_context->buffer.printf(L"%d-%d-%d %d:%d:%d - [%d] - in %s - %s: ",
		time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond, 
		threadid, 
		p_context->function ? p_context->function : L"<unknown function>",
		p_context->error ? L"*** ERROR " : L"");
	
}

void __trcf(TrcContext* p_context, const wchar_t* format, ...) {

	TrcBuffer& tb = p_context->buffer;

	if (tb.full()) {
		return;
	}

	traceLineStart(p_context);

	tb.cr();
	tb.tab();

	va_list ap;
	va_start(ap, format);
	tb.vprintf(format, ap);
	va_end(ap);
	tb.cr();

	tb.sanitize();
	__trc0(tb.ptr());

}

// helper for error handling. traces an error string plus the SQLITE error code.
void __trcWithSqliteError(TrcContext* p_context, int sqlite_errcode, const wchar_t* sqlite_errmsg, const wchar_t* format, ...) {

	TrcBuffer& tb = p_context->buffer;
	
	if (tb.full()) {
		return;
	}

	traceLineStart(p_context);
	tb.cr();

	tb.tab();
	va_list ap;
	va_start(ap, format);
	tb.vprintf(format, ap);
	va_end(ap);
	tb.cr();

	if (sqlite_errcode != SQLITE_OK) {
		tb.tab();
		tb.printf(L"   - (%d) %s", sqlite_errcode, describeErrorCode(sqlite_errcode));
		tb.cr();
	}

	if (sqlite_errmsg) {
		tb.tab();
		tb.printf(L"   - %s", sqlite_errmsg);
		tb.cr();
	}

	tb.sanitize();
	__trc0(tb.ptr());

} // end: handleSqliteError


// changes trace level and writes an information message
void changeTraceLevel(int newLvl) {

	const int oldLvl = g_traceLevel;

	if (newLvl == oldLvl) {
		return;
	}

	if (g_traceLevel > 0) {
		wchar_t buf[128];
		_snwprintf_s(buf, 128, _TRUNCATE, 
			L"------------------------------------------\n"
			L"Changing trace level %d -> %d.\n"
			L"------------------------------------------\n",
			oldLvl, newLvl);
		__trc0(buf);
	}

	g_traceLevel = newLvl;


	// if we did  trace to a file and now trace level is off, close file
	if (newLvl == 0) {
		if (g_traceFileHandle != INVALID_HANDLE_VALUE) {
			closeTraceFile();
		}
	}
	
	// if old level was 0, trace header.
	if (oldLvl == 0 && newLvl > 0) {
		traceHeader();
	}

};

void setTraceFileName(const wchar_t* v) {

	UNMANAGED_ASSERT( ! (g_traceFileHandle != INVALID_HANDLE_VALUE && g_traceFileName == NULL) );

	if (v && g_traceFileName && wcscmp(v, g_traceFileName) == 0) {
		return;
	}

	if (g_traceLevel > 0) {
		wchar_t buf[1024];
		_snwprintf_s(buf, 128, _TRUNCATE, 
			L"------------------------------------------\n"
			L"Changing trace file name to \"%s\".\n"
			L"------------------------------------------\n",
			(v ? v : L"<null>"));
		__trc0(buf);
	}

	if (g_traceFileHandle != INVALID_HANDLE_VALUE) {
		closeTraceFile();
		free (g_traceFileName);
		g_traceFileName = NULL;
	}
	
	g_traceFileName = _wcsdup(v);

}

const wchar_t* getTraceFileName() {
	return g_traceFileName;
}

void setTraceDestination(int v) {

	const int old = g_traceDestination;

	if (old == v) {
		return;
	}

	if (g_traceLevel > 0) {
		wchar_t buf[128];
		_snwprintf_s(buf, 128, _TRUNCATE, 
			L"------------------------------------------\n"
			L"Changing trace destination level %d -> %d.\n"
			L"------------------------------------------\n",
			old, v);
		__trc0(buf);
	}

	g_traceDestination = v;

	// if we did  trace to a file and now trace level is off, close file
	if (old == TRCDST_FILE) {
		if (g_traceFileHandle != INVALID_HANDLE_VALUE) {
			closeTraceFile();
		}
	}


}

int getTraceDestination() {
	return g_traceDestination;
}
